Java

Java

Java

Topics on Semeru (Java) on IBM Z

 View Only

PQC on Java: ML-KEM Tutorial (IBMJCECCA)

By Emily Popovic posted Fri June 13, 2025 11:30 AM

  

Overview

ML-KEM is a key encapsulation mechanism (KEM), whose security is based on the hardness of solving the learning-with-errors (LWE) problem over module lattices.

IBMJCECCA currently offers the following implementations:

  • ML-KEM 768

  • ML-KEM 1024

  • CRYSTALS-Kyber 1024 Round 2 (superseded by ML-KEM)

With ML-KEM, it is now possible to perform a quantum-safe hybrid key exchange scheme that combines the protection of traditional Elliptic Curve Cryptography (ECC) with the PQC ML-KEM algorithm. This hybrid key exchange scheme provides two layers of protection and can ensure that all key exchanges are protected from attacks by traditional and quantum computers.

This tutorial will demonstrate how to integrate ML-KEM into a Java application. Before you continue reading, please ensure your environment is properly configured by reading PQC on Java: Configuring your IBM Z system to use Quantum-safe algorithms.

Step 1. Generate Alice's keys

1.1. Generate ML-DSA keys

The first step is to create a ML-KEM key pair for Alice using the KeyPairGenerator and MLKEMKeyParameterSpec classes.

The MLKEMKeyParameterSpec class accepts the following values based on the desired implementation outlined in the Overview section:

  • kyber1024r2

  • mlkem768

  • mlkem1024

KeyPairGenerator mlkemKPG =
    KeyPairGenerator.getInstance("ML-KEM", "IBMJCECCA");
MLKEMKeyParameterSpec mlkemGenPS =
    new MLKEMKeyParameterSpec("mlkem1024");
mlkemKPG.initialize(mlkemGenPS, null);
KeyPair mlkemPairAlice = mlkemKPG.generateKeyPair();

1.2. Generate EC keys

Next, we must create an EC key pair for Alice using the IBMJCECCA provider.

ECGenParameterSpec ecGenPSAlice = new ECGenParameterSpec("secp256r1");
KeyPairGenerator ecKPGAlice = KeyPairGenerator.getInstance("EC", "IBMJCECCA");
ecKPGAlice.initialize(ecGenPSAlice, null);
KeyPair ecPairAlice = ecKPGAlice.generateKeyPair();

Step 2. Generate Bob's keys

2.1. Generate EC keys

Bob must also create his own EC key pair.

ECGenParameterSpec ecGenPSBob = new ECGenParameterSpec("secp256r1");
KeyPairGenerator ecKPGBob = KeyPairGenerator.getInstance("EC", "IBMJCECCA");
ecKPGBob.initialize(ecGenPSBob, null);
KeyPair ecPairBob = ecKPGBob.generateKeyPair();

2.2. Generate AES CIPHER key

Instead of creating his own ML-KEM keys, Bob must create his own AES CIPHER key. Only AES CIPHER keys are allowed for a ML-KEM key agreement.

CCAAlgorithmParameterSpec ccaAlgParmSpec =
    new CCAAlgorithmParameterSpec(256);
ccaAlgParmSpec.setKeyUsage(KeyUsage.OP_CIPHER);
KeyGenerator keyGen = KeyGenerator.getInstance("AES", "IBMJCECCA");
keyGen.init(ccaAlgParmSpec, null);
SecretKey aesKeyBob = keyGen.generateKey();

Step 3. Derive secret for Bob

Now that all of the necessary keys are created for Alice and Bob, we can begin the process of generating the key agreement. IBMJCECCA uses the classes MLKEMDerivationInput and MLKEMKeyAgreement classes to perform a ML-KEM key agreement.

MLKEMDerivationInput takes in an AES CIPHER key, ML-KEM public key, and a 16-byte initialization vector (IV) to perform a PKA Encrypt service via ICSF. This service generates a random 32B value which is then encrypted in two different ways:

  1. Using the ML-KEM public key (ML-KEM-encrypted value)

  2. Using the AES CIPHER key with the IV (AES-encrypted value)

These two different values will then be used with the MLKEMKeyAgreement class. The MLKEMKeyAgreement class takes in a series of keys and values and derives the final shared key agreement. The input for this class varies for Alice and Bob. See the steps below for a full rundown of the steps.

3.1. Create IV

First, we must derive a 16-byte initialization vector.

SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);

3.2. Use MLKEMDerivationInput to get encrypted values

We now have all of the inputs necessary to create the MLKEMDerivationInput class. The encrypted values are calculated after the encrypt() method is invoked.

MLKEMPublicKey mlkemPubAlice = (MLKEMPublicKey) mlkemPairAlice.getPublic();
MLKEMDerivationInput kdi =
    new MLKEMDerivationInput(aesKeyBob, mlkemPubAlice, iv);
kdi.encrypt();
byte[] aesEncryptedValue = kdi.getEncryptedAesValue();
byte[] mlkemEncryptedValue = kdi.getEncryptedMLKEMValue();

3.3. Create and initialize MLKEMKeyAgreement

Bob can now initialize a MLKEMKeyAgreement class with the information gathered from the previous steps.

ECPublicKey ecPubAlice = (ECPublicKey) ecPairAlice.getPublic();
ECPrivateHWKey ecPrivBob = (ECPrivateHWKey) ecPairBob.getPrivate();
MLKEMKeyAgreement bobMLKEMAgree = new MLKEMKeyAgreement();
bobMLKEMAgree.init(aesKeyBob, ecPrivBob, ecPubAlice, aesEncryptedValue, iv);

3.4. Generate shared secret

Finally, Bob just has to invoke the generateSecret() method to generate the final key to be shared between him and Alice.

The generateSecret() method accepts AES, DES, and DESede as the final shared secret algorithm.

AESKey bobSharedSecretKey = (AESKey) bobMLKEMAgree.generateSecret("AES");

Step 4. Derive secret for Alice

4.1. Create and initialize MLKEMKeyAgreement

Now that Bob has generated the shared secret, Alice must do the same. Alice does not need to perform a MLKEMDerivationInput because she will be using the ML-KEM-encrypted value generated when Bob performed that step. So, Alice can go right to initializing MLKEMKeyAgreement.

ECPublicKey ecPubBob = (ECPublicKey) ecPairBob.getPublic();
MLKEMPrivateKey mlkemPrivAlice = (MLKEMPrivateKey) mlkemPairAlice.getPrivate();
ECPrivateHWKey ecPrivAlice = (ECPrivateHWKey) ecPairAlice.getPrivate();
MLKEMKeyAgreement aliceMLKEMAgree = new MLKEMKeyAgreement();
aliceMLKEMAgree.init(mlkemPrivAlice, ecPrivAlice, ecPubBob, mlkemEncryptedValue, null);

4.2. Generate shared secret

Finally, Alice can invoke the generateSecret() method to generate the final key to be shared between her and Bob.

AESKey aliceSharedSecretKey = (AESKey) aliceMLKEMAgree.generateSecret("AES");

Step 5. Verify secrets match

Let's add in a quick check to verify that the shared secrets match.

if (Arrays.equals(aliceSharedSecretKey.getEncoded(),
    bobSharedSecretKey.getEncoded())
) {
    System.out.println("The secrets match!");
} else {
    System.out.println("The secrets DO NOT match.");
}

Example

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import com.ibm.crypto.hdwrCCA.provider.AESKey;
import com.ibm.crypto.hdwrCCA.provider.CCAAlgorithmParameterSpec;
import com.ibm.crypto.hdwrCCA.provider.ECPrivateHWKey;
import com.ibm.crypto.hdwrCCA.provider.MLKEMDerivationInput;
import com.ibm.crypto.hdwrCCA.provider.MLKEMKeyAgreement;
import com.ibm.crypto.hdwrCCA.provider.MLKEMKeyParameterSpec;
import com.ibm.crypto.hdwrCCA.provider.MLKEMPrivateKey;
import com.ibm.crypto.hdwrCCA.provider.MLKEMPublicKey;
import com.ibm.crypto.hdwrCCA.provider.SymmetricKeyConstants.KeyUsage;
import com.ibm.crypto.hdwrCCA.provider.ECPublicKey;

public class MLKEMExample {

    public static void main(String[] args) throws Exception {
        // -----------------------------------------------------------------
        // STEP 1: Generate Alice's keys
        // -----------------------------------------------------------------

        // 1.1. Generate ML-KEM keys
        KeyPairGenerator mlkemKPG =
            KeyPairGenerator.getInstance("ML-KEM", "IBMJCECCA");
        MLKEMKeyParameterSpec mlkemGenPS =
            new MLKEMKeyParameterSpec("mlkem1024");
        mlkemKPG.initialize(mlkemGenPS, null);
        KeyPair mlkemPairAlice = mlkemKPG.generateKeyPair();

        // 1.2. Generate EC keys
        ECGenParameterSpec ecGenPSAlice = new ECGenParameterSpec("secp256r1");
        KeyPairGenerator ecKPGAlice = KeyPairGenerator.getInstance("EC", "IBMJCECCA");
        ecKPGAlice.initialize(ecGenPSAlice, null);
        KeyPair ecPairAlice = ecKPGAlice.generateKeyPair();

        // -----------------------------------------------------------------
        // STEP 2: Generate Bob's keys
        // -----------------------------------------------------------------

        // 2.1. Generate EC keys
        ECGenParameterSpec ecGenPSBob = new ECGenParameterSpec("secp256r1");
        KeyPairGenerator ecKPGBob = KeyPairGenerator.getInstance("EC", "IBMJCECCA");
        ecKPGBob.initialize(ecGenPSBob, null);
        KeyPair ecPairBob = ecKPGBob.generateKeyPair();

        // 2.2 Generate AES CIPHER key
        CCAAlgorithmParameterSpec ccaAlgParmSpec =
            new CCAAlgorithmParameterSpec(256);
        ccaAlgParmSpec.setKeyUsage(KeyUsage.OP_CIPHER);
        KeyGenerator keyGen = KeyGenerator.getInstance("AES", "IBMJCECCA");
        keyGen.init(ccaAlgParmSpec, null);
        SecretKey aesKeyBob = keyGen.generateKey();

        // -----------------------------------------------------------------
        // STEP 3: Derive secret for Bob
        // -----------------------------------------------------------------

        // 3.1. Create IV
        SecureRandom random = new SecureRandom();
        byte[] iv = new byte[16];
        random.nextBytes(iv);

        // 3.2. Use MLKEMDerivationInput to get encrypted values
        MLKEMPublicKey mlkemPubAlice = (MLKEMPublicKey) mlkemPairAlice.getPublic();
        MLKEMDerivationInput kdi =
            new MLKEMDerivationInput(aesKeyBob, mlkemPubAlice, iv);
        kdi.encrypt();
        byte[] aesEncryptedValue = kdi.getEncryptedAesValue();
        byte[] mlkemEncryptedValue = kdi.getEncryptedMLKEMValue();

        // 3.3. Create and initialize MLKEMKeyAgreement
        ECPublicKey ecPubAlice = (ECPublicKey) ecPairAlice.getPublic();
        ECPrivateHWKey ecPrivBob = (ECPrivateHWKey) ecPairBob.getPrivate();
        MLKEMKeyAgreement bobMLKEMAgree = new MLKEMKeyAgreement();
        bobMLKEMAgree.init(aesKeyBob, ecPrivBob, ecPubAlice, aesEncryptedValue, iv);

        // 3.4. Generate shared secret
        AESKey bobSharedSecretKey = (AESKey) bobMLKEMAgree.generateSecret("AES");

        // -----------------------------------------------------------------
        // STEP 4: Derive secret for Alice
        // -----------------------------------------------------------------

        // 4.1. Create and initialize MLKEMKeyAgreement
        ECPublicKey ecPubBob = (ECPublicKey) ecPairBob.getPublic();
        MLKEMPrivateKey mlkemPrivAlice = (MLKEMPrivateKey) mlkemPairAlice.getPrivate();
        ECPrivateHWKey ecPrivAlice = (ECPrivateHWKey) ecPairAlice.getPrivate();
        MLKEMKeyAgreement aliceMLKEMAgree = new MLKEMKeyAgreement();
        aliceMLKEMAgree.init(mlkemPrivAlice, ecPrivAlice, ecPubBob, mlkemEncryptedValue, null);

        // 4.2. Generate shared secret
        AESKey aliceSharedSecretKey = (AESKey) aliceMLKEMAgree.generateSecret("AES");

        // -----------------------------------------------------------------
        // STEP 5: Verify secrets match
        // -----------------------------------------------------------------

        if (Arrays.equals(aliceSharedSecretKey.getEncoded(),
            bobSharedSecretKey.getEncoded())
        ) {
            System.out.println("The secrets match!");
        } else {
            System.out.println("The secrets DO NOT match.");
        }
    }
}

Output

The previous Example should produce the following output:

The secrets match!

Conclusion

In this tutorial, we learned how to use the ML-KEM PQC algorithm with IBMJCECCA to create keys and generate a shared secret between two parties. Thanks for reading!

If you have additional questions, please email me at Emily.Popovic1@ibm.com.

References

  1. PQC on Java: Configuring your IBM Z system to use Post-Quantum Cryptography

  2. ML-KEM, CRYSTALS-Kyber Key Encapsulation Mechanism

  3. z17 IBM Semeru Runtimes: PQC Cryptography Enhancements

0 comments
5 views

Permalink