From 3942042a46a0d4c9a6c4e7799c719e0fb7cd8f2a Mon Sep 17 00:00:00 2001 From: Jason Katonica Date: Sun, 3 May 2026 06:37:54 -0400 Subject: [PATCH] improve ML-KEM algorithm handling and generic test - Fix ML-KEM encapsulation length calculation to use actual key algorithm instead of generic 'ML-KEM' string - Add validation for encapsulation message length in decapsulation - Improve key conversion to use actual algorithm from key instead of generic algorithm parameter - Add comprehensive test for invalid encapsulation length handling - Refactor test skip conditions to use assumeFalse for better JUnit 5 compatibility - Add new interoperability tests using NamedParameterSpec: * testMLKEMInteropWithNamedParameterSpec * testMLKEMInteropEmptyParamsWithNamedParameterSpec * testMLKEMInteropSmallerSecretWithNamedParameterSpec * testMLKEMBidirectionalInteropWithNamedParameterSpec - Remove test unused imports and improve code consistency This ensures ML-KEM operations correctly handle different parameter sets (ML-KEM-512, ML-KEM-768, ML-KEM-1024) and provides better error messages when encapsulation length mismatches occur. Signed-off-by: Jason Katonica --- .../plus/provider/DefaultProviderAttrs.java | 6 +- .../ibm/crypto/plus/provider/MLKEMImpl.java | 57 +++- .../crypto/plus/provider/PQCKeyFactory.java | 23 +- src/test/ProviderDefAttrs.config | 8 +- .../ibm/jceplus/junit/base/BaseTestKEM.java | 51 ++++ .../junit/base/BaseTestPQCKeyInterop.java | 268 +++++++++++++----- 6 files changed, 331 insertions(+), 82 deletions(-) diff --git a/src/main/java/com/ibm/crypto/plus/provider/DefaultProviderAttrs.java b/src/main/java/com/ibm/crypto/plus/provider/DefaultProviderAttrs.java index 08fa8ad95..4e4021e61 100644 --- a/src/main/java/com/ibm/crypto/plus/provider/DefaultProviderAttrs.java +++ b/src/main/java/com/ibm/crypto/plus/provider/DefaultProviderAttrs.java @@ -147,9 +147,10 @@ class DefaultProviderAttrs { + " # PQC key factories\n" + " # =======================================================================\n" + " #\n" + + "Service.KeyFactory.ML-KEM = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM\n" + "KeyFactory.ML-KEM-512.alias.add = ML_KEM_512, MLKEM512, OID.2.16.840.1.101.3.4.4.1, 2.16.840.1.101.3.4.4.1\n" + "Service.KeyFactory.ML-KEM-512 = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM512\n" - + "KeyFactory.ML-KEM-768.alias.add = ML-KEM, ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2\n" + + "KeyFactory.ML-KEM-768.alias.add = ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2\n" + "Service.KeyFactory.ML-KEM-768 = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM768\n" + "KeyFactory.ML-KEM-1024.alias.add = ML_KEM_1024, MLKEM1024, OID.2.16.840.1.101.3.4.4.3, 2.16.840.1.101.3.4.4.3\n" + "Service.KeyFactory.ML-KEM-1024 = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM1024\n" @@ -315,9 +316,10 @@ class DefaultProviderAttrs { + " # PQC key encapsulation mechanisms\n" + " # =======================================================================\n" + " #\n" + + "Service.KEM.ML-KEM = com.ibm.crypto.plus.provider.MLKEMImpl$MLKEM\n" + "KEM.ML-KEM-512.alias.add = ML_KEM_512, MLKEM512, OID.2.16.840.1.101.3.4.4.1, 2.16.840.1.101.3.4.4.1\n" + "Service.KEM.ML-KEM-512 = com.ibm.crypto.plus.provider.MLKEMImpl$MLKEM512\n" - + "KEM.ML-KEM-768.alias.add = ML-KEM, ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2\n" + + "KEM.ML-KEM-768.alias.add = ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2\n" + "Service.KEM.ML-KEM-768 = com.ibm.crypto.plus.provider.MLKEMImpl$MLKEM768\n" + "KEM.ML-KEM-1024.alias.add = ML_KEM_1024, MLKEM1024, OID.2.16.840.1.101.3.4.4.3, 2.16.840.1.101.3.4.4.3\n" diff --git a/src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java b/src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java index ea231af41..d71dd4ae3 100644 --- a/src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java +++ b/src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java @@ -38,18 +38,22 @@ public MLKEMImpl(OpenJCEPlusProvider provider, String alg) { this.alg = alg; } - private int getEncapsulationLength() { + private int getEncapsulationLength(String algorithm) { int size = 0; - switch (this.alg) { + switch (algorithm) { case "ML-KEM-512": size = 768; break; case "ML-KEM-768": size = 1088; break; - default: + case "ML-KEM-1024": size = 1568; + break; + default: + // If algorithm is generic "ML-KEM", default to ML-KEM-768 + size = 1088; } return size; } @@ -72,8 +76,15 @@ public KEMSpi.EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, if (!(pubKey instanceof PQCPublicKey)) { // Try and convert this key to a usage PQCPublicKey + // First verify it's an ML-KEM key + String keyAlgorithm = publicKey.getAlgorithm(); + if (keyAlgorithm == null || !keyAlgorithm.startsWith("ML-KEM")) { + throw new InvalidKeyException("unsupported key"); + } + + // Use the key's actual algorithm, not the generic "ML-KEM" try { - KeyFactory kf = KeyFactory.getInstance(this.alg, this.provider.getName()); + KeyFactory kf = KeyFactory.getInstance(keyAlgorithm, this.provider.getName()); EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKey.getEncoded()); pubKey = kf.generatePublic(publicKeySpec); @@ -105,7 +116,9 @@ class MLKEMEncapsulator implements KEMSpi.EncapsulatorSpi { @Override public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { - int encapLen = getEncapsulationLength(); + // Get the actual algorithm from the public key + String keyAlgorithm = publicKey.getAlgorithm(); + int encapLen = getEncapsulationLength(keyAlgorithm); byte[] encapsulation = new byte[encapLen]; byte[] secret = new byte[SECRETSIZE]; @@ -130,7 +143,8 @@ public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { @Override public int engineEncapsulationSize() { - return getEncapsulationLength(); + String keyAlgorithm = publicKey.getAlgorithm(); + return getEncapsulationLength(keyAlgorithm); } @Override @@ -155,9 +169,16 @@ public KEMSpi.DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, if (!(privKey instanceof PQCPrivateKey)) { // Try and convert this key to a usage PQCPrivateKey + // First verify it's an ML-KEM key + String keyAlgorithm = privateKey.getAlgorithm(); + if (keyAlgorithm == null || !keyAlgorithm.startsWith("ML-KEM")) { + throw new InvalidKeyException("unsupported key"); + } + + // Use the key's actual algorithm, not the generic "ML-KEM" byte[] encoding = null; try { - KeyFactory kf = KeyFactory.getInstance(this.alg, this.provider.getName()); + KeyFactory kf = KeyFactory.getInstance(keyAlgorithm, this.provider.getName()); encoding = privateKey.getEncoded(); PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encoding); privKey = kf.generatePrivate(privateKeySpec); @@ -197,6 +218,17 @@ public SecretKey engineDecapsulate(byte[] cipherText, int from, int to, String a if (algorithm == null || cipherText == null) { throw new NullPointerException(); } + + // Validate encapsulation length matches the key's algorithm + String keyAlgorithm = privateKey.getAlgorithm(); + int expectedEncapLen = getEncapsulationLength(keyAlgorithm); + if (cipherText.length != expectedEncapLen) { + throw new DecapsulateException( + "Invalid key encapsulation message length: expected " + + expectedEncapLen + " bytes for " + keyAlgorithm + + ", but got " + cipherText.length + " bytes"); + } + try { secret = OJPKEM.KEM_decapsulate(((PQCPrivateKey) this.privateKey).getPQCKey().getPKeyId(), cipherText, provider); @@ -210,8 +242,8 @@ public SecretKey engineDecapsulate(byte[] cipherText, int from, int to, String a @Override public int engineEncapsulationSize() { - - return getEncapsulationLength(); + String keyAlgorithm = privateKey.getAlgorithm(); + return getEncapsulationLength(keyAlgorithm); } @Override @@ -222,6 +254,13 @@ public int engineSecretSize() { } + public static final class MLKEM extends MLKEMImpl { + + public MLKEM(OpenJCEPlusProvider provider) { + super(provider, "ML-KEM"); + } + } + public static final class MLKEM512 extends MLKEMImpl { public MLKEM512(OpenJCEPlusProvider provider) { diff --git a/src/main/java/com/ibm/crypto/plus/provider/PQCKeyFactory.java b/src/main/java/com/ibm/crypto/plus/provider/PQCKeyFactory.java index be54055ca..12e9bc983 100644 --- a/src/main/java/com/ibm/crypto/plus/provider/PQCKeyFactory.java +++ b/src/main/java/com/ibm/crypto/plus/provider/PQCKeyFactory.java @@ -188,8 +188,20 @@ private void checkKeyAlgo(Key key) throws InvalidKeyException { String keyAlg = key.getAlgorithm(); if (keyAlg == null) { throw new InvalidKeyException("Algorithm associate with key is null."); - } else if (!(key.getAlgorithm().equalsIgnoreCase(this.algName) || - (PQCKnownOIDs.findMatch(key.getAlgorithm()).stdName().equalsIgnoreCase(this.algName)))) { + } + + // Check if algorithms match exactly or via OID lookup + boolean matches = key.getAlgorithm().equalsIgnoreCase(this.algName) || + (PQCKnownOIDs.findMatch(key.getAlgorithm()).stdName().equalsIgnoreCase(this.algName)); + + // Special case for generic ML-KEM: Allow any ML-KEM parameter set variant + // (ML-KEM-512, ML-KEM-768, ML-KEM-1024) when using the generic "ML-KEM" KeyFactory. + // This enables interoperability with KEM.getInstance("ML-KEM", ...). + if (!matches && "ML-KEM".equals(this.algName) && keyAlg.startsWith("ML-KEM")) { + matches = true; + } + + if (!matches) { throw new InvalidKeyException("Expected a " + this.algName + " key, but got " + keyAlg); } @@ -217,6 +229,13 @@ private boolean checkEncoded(byte[] key, boolean pub) { } } + public static final class MLKEM extends PQCKeyFactory { + + public MLKEM(OpenJCEPlusProvider provider) { + super(provider, "ML-KEM"); + } + } + public static final class MLKEM512 extends PQCKeyFactory { public MLKEM512(OpenJCEPlusProvider provider) { diff --git a/src/test/ProviderDefAttrs.config b/src/test/ProviderDefAttrs.config index 53e614f15..e66335a7f 100644 --- a/src/test/ProviderDefAttrs.config +++ b/src/test/ProviderDefAttrs.config @@ -228,10 +228,12 @@ Service.KeyFactory.RSAPSS = com.ibm.crypto.plus.provider.RSAKeyFactory$PSS # PQC key factories # ======================================================================= # +Service.KeyFactory.ML-KEM = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM + KeyFactory.ML-KEM-512.alias.add = ML_KEM_512, MLKEM512, OID.2.16.840.1.101.3.4.4.1, 2.16.840.1.101.3.4.4.1 Service.KeyFactory.ML-KEM-512 = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM512 -KeyFactory.ML-KEM-768.alias.add = ML-KEM, ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2 +KeyFactory.ML-KEM-768.alias.add = ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2 Service.KeyFactory.ML-KEM-768 = com.ibm.crypto.plus.provider.PQCKeyFactory$MLKEM768 KeyFactory.ML-KEM-1024.alias.add = ML_KEM_1024, MLKEM1024, OID.2.16.840.1.101.3.4.4.3, 2.16.840.1.101.3.4.4.3 @@ -464,10 +466,12 @@ Service.MessageDigest.SHA3-512 = com.ibm.crypto.plus.provider.MessageDigest$SHA3 # PQC key encapsulation mechanisms # ======================================================================= # +Service.KEM.ML-KEM = com.ibm.crypto.plus.provider.MLKEMImpl$MLKEM + KEM.ML-KEM-512.alias.add = ML_KEM_512, MLKEM512, OID.2.16.840.1.101.3.4.4.1, 2.16.840.1.101.3.4.4.1 Service.KEM.ML-KEM-512 = com.ibm.crypto.plus.provider.MLKEMImpl$MLKEM512 -KEM.ML-KEM-768.alias.add = ML-KEM, ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2 +KEM.ML-KEM-768.alias.add = ML_KEM_768, MLKEM768, OID.2.16.840.1.101.3.4.4.2, 2.16.840.1.101.3.4.4.2 Service.KEM.ML-KEM-768 = com.ibm.crypto.plus.provider.MLKEMImpl$MLKEM768 KEM.ML-KEM-1024.alias.add = ML_KEM_1024, MLKEM1024, OID.2.16.840.1.101.3.4.4.3, 2.16.840.1.101.3.4.4.3 diff --git a/src/test/java/ibm/jceplus/junit/base/BaseTestKEM.java b/src/test/java/ibm/jceplus/junit/base/BaseTestKEM.java index 70587460a..73ea09617 100644 --- a/src/test/java/ibm/jceplus/junit/base/BaseTestKEM.java +++ b/src/test/java/ibm/jceplus/junit/base/BaseTestKEM.java @@ -210,6 +210,57 @@ public void testKEMKeys(String Algorithm) throws Exception { } } + /** + * Tests that decapsulation fails with a DecapsulateException when attempting to decapsulate + * an encapsulation message that was created with a different ML-KEM algorithm variant. + * + *

This test verifies that the KEM implementation properly validates the encapsulation + * message length during decapsulation. Each ML-KEM variant (ML-KEM-512, ML-KEM-768, ML-KEM-1024) + * produces encapsulation messages of different lengths. When a decapsulator receives an + * encapsulation message with an incorrect length (from a different variant), it should + * reject it with a DecapsulateException containing an appropriate error message. + * + *

Test procedure: + *

    + *
  1. Generate a key pair using the first algorithm (keyAlgorithm)
  2. + *
  3. Generate a different key pair using a second algorithm (wrongAlgorithm)
  4. + *
  5. Create an encapsulation using the second key pair (wrong length for first algorithm)
  6. + *
  7. Attempt to decapsulate using the first key pair's private key
  8. + *
  9. Verify that a DecapsulateException is thrown with the expected error message
  10. + *
+ * + * @param keyAlgorithm the ML-KEM algorithm variant to use for the decapsulation key pair + * @param wrongAlgorithm the ML-KEM algorithm variant to use for creating the encapsulation + * (produces wrong length for keyAlgorithm) + * @throws Exception if an unexpected error occurs during test execution + */ + @ParameterizedTest + @CsvSource({"ML-KEM-512,ML-KEM-768", "ML-KEM-768,ML-KEM-1024", "ML-KEM-1024,ML-KEM-512"}) + public void testKEMInvalidEncapsulationLength(String keyAlgorithm, String wrongAlgorithm) throws Exception { + // Generate a key pair with one algorithm + KeyPair keyPair = generateKeyPair(keyAlgorithm); + + // Create encapsulation with a different algorithm (wrong length) + KEM kemWrong = KEM.getInstance(wrongAlgorithm, getProviderName()); + KeyPair wrongKeyPair = generateKeyPair(wrongAlgorithm); + KEM.Encapsulator encapsulator = kemWrong.newEncapsulator(wrongKeyPair.getPublic()); + KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 32, "AES"); + + // Try to decapsulate with the original key (wrong length) + KEM kem = KEM.getInstance(keyAlgorithm, getProviderName()); + KEM.Decapsulator decapsulator = kem.newDecapsulator(keyPair.getPrivate()); + + try { + decapsulator.decapsulate(encapsulated.encapsulation(), 0, 32, "AES"); + fail("testKEMInvalidEncapsulationLength failed - Invalid encapsulation length did not cause a DecapsulateException for " + keyAlgorithm + " with " + wrongAlgorithm + " encapsulation"); + } catch (javax.crypto.DecapsulateException de) { + assertTrue(de.getMessage().contains("Invalid key encapsulation message length"), + "Expected error message about invalid encapsulation length, but got: " + de.getMessage()); + assertTrue(de.getMessage().contains(keyAlgorithm), + "Expected error message to mention key algorithm " + keyAlgorithm + ", but got: " + de.getMessage()); + } + } + protected KeyPair generateKeyPair(String Algorithm) throws Exception { pqcKeyPairGen = KeyPairGenerator.getInstance(Algorithm, getProviderName()); diff --git a/src/test/java/ibm/jceplus/junit/base/BaseTestPQCKeyInterop.java b/src/test/java/ibm/jceplus/junit/base/BaseTestPQCKeyInterop.java index 837d09c0c..9d8240f5a 100644 --- a/src/test/java/ibm/jceplus/junit/base/BaseTestPQCKeyInterop.java +++ b/src/test/java/ibm/jceplus/junit/base/BaseTestPQCKeyInterop.java @@ -16,6 +16,7 @@ import java.security.PublicKey; import java.security.Signature; import java.security.spec.EncodedKeySpec; +import java.security.spec.NamedParameterSpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; @@ -44,10 +45,8 @@ public void testPQCKeyGenKEM_PlusToInterop() throws Exception { String pqcAlgorithm = "ML-KEM-512"; boolean same = false; - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet. - return; - } + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); keyPairGenPlus = KeyPairGenerator.getInstance(pqcAlgorithm, getProviderName()); keyFactoryPlus = KeyFactory.getInstance(pqcAlgorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(pqcAlgorithm, getInteropProviderName()); @@ -111,10 +110,8 @@ public void testPQCKeyGenKEM_Interop() throws Exception { String pqcAlgorithm = "ML-KEM-512"; boolean same = false; - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet. - return; - } + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); keyPairGenPlus = KeyPairGenerator.getInstance(pqcAlgorithm, getProviderName()); keyFactoryPlus = KeyFactory.getInstance(pqcAlgorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(pqcAlgorithm, getInteropProviderName()); @@ -148,11 +145,10 @@ public void testPQCKeyGenKEM_PlusToInteropRAW() throws Exception { String pqcAlgorithm = "ML-KEM-512"; boolean same = false; - if (getProviderName().equals("OpenJCEPlusFIPS") || - getInteropProviderName().equals(Utils.PROVIDER_BC)) { - //This is not in the FIPS provider yet and Boucy Castle does not support this test. - return; - } + //This is not in the FIPS provider yet and Bouncy Castle does not support this test. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + keyPairGenPlus = KeyPairGenerator.getInstance(pqcAlgorithm, getProviderName()); keyFactoryPlus = KeyFactory.getInstance(pqcAlgorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(pqcAlgorithm, getInteropProviderName()); @@ -181,10 +177,9 @@ public void testPQCKeyGenMLDSA_PlusToInterop() throws Exception { String pqcAlgorithm = "ML-DSA-65"; boolean same = false; - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet. - return; - } + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + keyPairGenPlus = KeyPairGenerator.getInstance(pqcAlgorithm, getProviderName()); keyFactoryPlus = KeyFactory.getInstance(pqcAlgorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(pqcAlgorithm, getInteropProviderName2()); @@ -217,10 +212,9 @@ public void testPQCKeyGenMLDSA_Interop() throws Exception { String pqcAlgorithm = "ML-DSA-65"; boolean same = false; - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet. - return; - } + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + keyPairGenPlus = KeyPairGenerator.getInstance(pqcAlgorithm, getProviderName()); keyFactoryPlus = KeyFactory.getInstance(pqcAlgorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(pqcAlgorithm, getInteropProviderName2()); @@ -253,11 +247,10 @@ public void testPQCKeyGenMLDSA_PlusToInteropRAW() throws Exception { String pqcAlgorithm = "ML-DSA-65"; boolean same = false; - if (getProviderName().equals("OpenJCEPlusFIPS") || - getInteropProviderName().equals(Utils.PROVIDER_BC)) { - //This is not in the FIPS provider yet and Bouncy Castle does not support this test. - return; - } + //This is not in the FIPS provider yet and Bouncy Castle does not support this test. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + keyPairGenPlus = KeyPairGenerator.getInstance(pqcAlgorithm, getProviderName()); keyFactoryPlus = KeyFactory.getInstance(pqcAlgorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(pqcAlgorithm, getInteropProviderName2()); @@ -303,14 +296,11 @@ protected KeyPair generateKeyPair(KeyPairGenerator keyPairGen) throws Exception @ParameterizedTest @CsvSource({"ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) public void testSignInteropAndVerifyPlus(String algorithm) throws Exception { + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(algorithm.equalsIgnoreCase("ML-DSA") && getInteropProviderName2().equalsIgnoreCase("BC")); + try { - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet. - return; - } - if (algorithm.equalsIgnoreCase("ML-DSA") && getInteropProviderName2().equalsIgnoreCase("BC")) { - return; - } keyPairGenInterop = KeyPairGenerator.getInstance(algorithm, getInteropProviderName2()); KeyPair keyPairInterop = generateKeyPair(keyPairGenInterop); @@ -341,12 +331,11 @@ public void testSignInteropAndVerifyPlus(String algorithm) throws Exception { @ParameterizedTest @CsvSource({"ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) public void testSignInteropKeysPlusSignVerify(String algorithm) { + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName2())); + try { - if (getProviderName().equals("OpenJCEPlusFIPS") || - getInteropProviderName().equals(Utils.PROVIDER_BC)) { - //This is not in the FIPS provider yet. - return; - } keyPairGenInterop = KeyPairGenerator.getInstance(algorithm, getInteropProviderName2()); KeyPair keyPairInterop = generateKeyPair(keyPairGenInterop); @@ -376,12 +365,11 @@ public void testSignInteropKeysPlusSignVerify(String algorithm) { @ParameterizedTest @CsvSource({"ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) public void testSignPlusKeysInteropSignVerify(String algorithm) { + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName2())); + try { - if (getProviderName().equals("OpenJCEPlusFIPS") || - getInteropProviderName().equals(Utils.PROVIDER_BC)) { - //This is not in the FIPS provider yet. - return; - } keyPairGenPlus = KeyPairGenerator.getInstance(algorithm, getProviderName()); KeyPair keyPairPlus = generateKeyPair(keyPairGenPlus); @@ -412,10 +400,8 @@ public void testSignPlusKeysInteropSignVerify(String algorithm) { @CsvSource({"ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) public void testSignPlusAndVerifyInterop(String algorithm) { try { - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet. - return; - } + //This is not in the FIPS provider yet. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); keyPairGenPlus = KeyPairGenerator.getInstance(algorithm, getProviderName()); KeyPair keyPairPlus = generateKeyPair(keyPairGenPlus); @@ -447,13 +433,11 @@ public void testSignPlusAndVerifyInterop(String algorithm) { @ParameterizedTest @CsvSource({"ML-KEM", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) public void testKEMPlusKeyInteropAll(String Algorithm) { - try { - if (getProviderName().equals("OpenJCEPlusFIPS") || - getInteropProviderName().equals(Utils.PROVIDER_BC)) { - //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. - return; - } + //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + try { KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); keyPairGenPlus = KeyPairGenerator.getInstance(Algorithm, getProviderName()); @@ -489,13 +473,11 @@ public void testKEMPlusKeyInteropAll(String Algorithm) { @ParameterizedTest @CsvSource({"ML-KEM", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) public void testKEMInteropKeyPlusAll(String Algorithm) { - try { - if (getProviderName().equals("OpenJCEPlusFIPS") || - getInteropProviderName().equals(Utils.PROVIDER_BC)) { - //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. - return; - } + //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + try { KEM kemPlus = KEM.getInstance(Algorithm, getProviderName()); keyPairGenInterop = KeyPairGenerator.getInstance(Algorithm, getInteropProviderName()); @@ -532,10 +514,8 @@ public void testKEMInteropKeyPlusAll(String Algorithm) { @CsvSource({"ML-KEM", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) public void testKEMPlusCreatesInteropGet(String Algorithm) { try { - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. - return; - } + //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); KEM kemPlus = KEM.getInstance(Algorithm, getProviderName()); KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); @@ -572,10 +552,8 @@ public void testKEMPlusCreatesInteropGet(String Algorithm) { @CsvSource({"ML-KEM", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) public void testKEMInteropCreatesPlusGet(String Algorithm) { try { - if (getProviderName().equals("OpenJCEPlusFIPS")) { - //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. - return; - } + //This is not in the FIPS provider yet and Oracle Private keys have an extra Octet in them. + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); KEM kemPlus = KEM.getInstance(Algorithm, getProviderName()); KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); @@ -605,4 +583,160 @@ public void testKEMInteropCreatesPlusGet(String Algorithm) { } } + /** + * Test ML-KEM interoperability using NamedParameterSpec to initialize KeyPairGenerator. + * Tests encapsulation / decapsulation with different providers. + * + * @param parameterSet The ML-KEM parameter set (ML-KEM-512, ML-KEM-768, ML-KEM-1024) + * @throws Exception if any cryptographic operation fails + */ + @ParameterizedTest + @CsvSource({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) + public void testMLKEMInteropWithNamedParameterSpec(String parameterSet) throws Exception { + // Not in FIPS provider yet and BC doesn't support this test + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + + // Generate key pair using NamedParameterSpec with provider + KeyPairGenerator keyPairGenPlus = KeyPairGenerator.getInstance("ML-KEM", getProviderName()); + keyPairGenPlus.initialize(new NamedParameterSpec(parameterSet)); + KeyPair keyPairPlus = generateKeyPair(keyPairGenPlus); + + // Encapsulate using provider + KEM kemPlus = KEM.getInstance("ML-KEM", getProviderName()); + KEM.Encapsulator encapsulator = kemPlus.newEncapsulator(keyPairPlus.getPublic()); + KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 32, "AES"); + + SecretKey encapKey = encapsulated.key(); + byte[] encapsulation = encapsulated.encapsulation(); + + // Decapsulate using interop provider + KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); + KEM.Decapsulator decapsulator = kemInterop.newDecapsulator(keyPairPlus.getPrivate()); + SecretKey decapKey = decapsulator.decapsulate(encapsulation, 0, 32, "AES"); + + // Verify that both keys match + assertArrayEquals(encapKey.getEncoded(), decapKey.getEncoded(), + "Encapsulated and decapsulated keys do not match for " + parameterSet); + } + + /** + * Test ML-KEM interoperability with empty parameters using NamedParameterSpec. + * Tests encapsulation and decapsulation without from/to specification. + * + * @param parameterSet The ML-KEM parameter set (ML-KEM-512, ML-KEM-768, ML-KEM-1024) + * @throws Exception if any cryptographic operation fails + */ + @ParameterizedTest + @CsvSource({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) + public void testMLKEMInteropEmptyParamsWithNamedParameterSpec(String parameterSet) throws Exception { + // Not in FIPS provider yet and BC doesn't support this test + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + + // Generate key pair using NamedParameterSpec with interop provider + KeyPairGenerator keyPairGenInterop = KeyPairGenerator.getInstance("ML-KEM", getInteropProviderName()); + keyPairGenInterop.initialize(new NamedParameterSpec(parameterSet)); + KeyPair keyPairInterop = generateKeyPair(keyPairGenInterop); + + // Encapsulate using interop provider (no from/to parameters) + KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); + KEM.Encapsulator encapsulator = kemInterop.newEncapsulator(keyPairInterop.getPublic()); + KEM.Encapsulated encapsulated = encapsulator.encapsulate(); + + SecretKey encapKey = encapsulated.key(); + byte[] encapsulation = encapsulated.encapsulation(); + + // Decapsulate using provider (no from/to parameters) + KEM kemPlus = KEM.getInstance("ML-KEM", getProviderName()); + KEM.Decapsulator decapsulator = kemPlus.newDecapsulator(keyPairInterop.getPrivate()); + SecretKey decapKey = decapsulator.decapsulate(encapsulation); + + // Verify that both keys match + assertArrayEquals(encapKey.getEncoded(), decapKey.getEncoded(), + "Encapsulated and decapsulated keys do not match for " + parameterSet); + } + + /** + * Test ML-KEM interoperability with smaller secret size using NamedParameterSpec. + * Tests with 16 bytes instead of the default 32 bytes. + * + * @param parameterSet The ML-KEM parameter set (ML-KEM-512, ML-KEM-768, ML-KEM-1024) + * @throws Exception if any cryptographic operation fails + */ + @ParameterizedTest + @CsvSource({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) + public void testMLKEMInteropSmallerSecretWithNamedParameterSpec(String parameterSet) throws Exception { + // Not in FIPS provider yet and BC doesn't support this test + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + + // Generate key pair using NamedParameterSpec with provider + KeyPairGenerator keyPairGenPlus = KeyPairGenerator.getInstance("ML-KEM", getProviderName()); + keyPairGenPlus.initialize(new NamedParameterSpec(parameterSet)); + KeyPair keyPairPlus = generateKeyPair(keyPairGenPlus); + + // Encapsulate using provider with smaller secret (16 bytes) + KEM kemPlus = KEM.getInstance("ML-KEM", getProviderName()); + KEM.Encapsulator encapsulator = kemPlus.newEncapsulator(keyPairPlus.getPublic()); + KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 16, "AES"); + + SecretKey encapKey = encapsulated.key(); + byte[] encapsulation = encapsulated.encapsulation(); + + // Decapsulate using interop provider with same secret size + KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); + KEM.Decapsulator decapsulator = kemInterop.newDecapsulator(keyPairPlus.getPrivate()); + SecretKey decapKey = decapsulator.decapsulate(encapsulation, 0, 16, "AES"); + + // Verify that both keys match + assertArrayEquals(encapKey.getEncoded(), decapKey.getEncoded(), + "Encapsulated and decapsulated keys do not match for " + parameterSet); + } + + /** + * Test bidirectional ML-KEM interoperability using NamedParameterSpec. + * Tests both directions to and from providers. + * + * @param parameterSet The ML-KEM parameter set (ML-KEM-512, ML-KEM-768, ML-KEM-1024) + * @throws Exception if any cryptographic operation fails + */ + @ParameterizedTest + @CsvSource({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) + public void testMLKEMBidirectionalInteropWithNamedParameterSpec(String parameterSet) throws Exception { + // Not in FIPS provider yet and BC doesn't support this test + assumeFalse("OpenJCEPlusFIPS".equals(getProviderName())); + assumeFalse(Utils.PROVIDER_BC.equals(getInteropProviderName())); + + // Test 1: Generate with provider, encapsulate with interop provider, decapsulate with provider + KeyPairGenerator keyPairGenPlus = KeyPairGenerator.getInstance("ML-KEM", getProviderName()); + keyPairGenPlus.initialize(new NamedParameterSpec(parameterSet)); + KeyPair keyPairPlus = generateKeyPair(keyPairGenPlus); + + KEM kemInterop = KEM.getInstance("ML-KEM", getInteropProviderName()); + KEM.Encapsulator encapsulatorInterop = kemInterop.newEncapsulator(keyPairPlus.getPublic()); + KEM.Encapsulated encapsulatedInterop = encapsulatorInterop.encapsulate(0, 32, "AES"); + + KEM kemPlus = KEM.getInstance("ML-KEM", getProviderName()); + KEM.Decapsulator decapsulatorPlus = kemPlus.newDecapsulator(keyPairPlus.getPrivate()); + SecretKey decapKeyPlus = decapsulatorPlus.decapsulate(encapsulatedInterop.encapsulation(), 0, 32, "AES"); + + assertArrayEquals(encapsulatedInterop.key().getEncoded(), decapKeyPlus.getEncoded(), + "Keys do not match for test 1 with " + parameterSet); + + // Test 2: Generate with interop provider, encapsulate with provider, decapsulate with interop provider + KeyPairGenerator keyPairGenInterop = KeyPairGenerator.getInstance("ML-KEM", getInteropProviderName()); + keyPairGenInterop.initialize(new NamedParameterSpec(parameterSet)); + KeyPair keyPairInterop = generateKeyPair(keyPairGenInterop); + + KEM.Encapsulator encapsulatorPlus = kemPlus.newEncapsulator(keyPairInterop.getPublic()); + KEM.Encapsulated encapsulatedPlus = encapsulatorPlus.encapsulate(0, 32, "AES"); + + KEM.Decapsulator decapsulatorInterop = kemInterop.newDecapsulator(keyPairInterop.getPrivate()); + SecretKey decapKeyInterop = decapsulatorInterop.decapsulate(encapsulatedPlus.encapsulation(), 0, 32, "AES"); + + assertArrayEquals(encapsulatedPlus.key().getEncoded(), decapKeyInterop.getEncoded(), + "Keys do not match for test 2 with " + parameterSet); + } + }