JDK-8316701 : Generation of EC key pairs changed with EdDSA
  • Type: Bug
  • Component: security-libs
  • Sub-Component: javax.crypto
  • Affected Version: 15,17,21,22
  • Priority: P3
  • Status: New
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2023-09-20
  • Updated: 2023-09-24
Related Reports
Relates :  
Description
A DESCRIPTION OF THE PROBLEM :
Our application uses TLS key pairs for securing communications. To simplify key pair maintenance and to avoid having to manage a CA on the side, we have a tool capable of generating signed key pairs by internally generating a CA from a user defined seed. Doing so, allow us to keep things simple for the user (who only has two small pieces of information related to the seed to deal with) and still be able to use TLS securely.
The tool can also generate a keystore with the generated TLS key pair and the CA certificate.
We then rely on the ability to deterministically generate the same CA every time we need key pair generation. The CA is using EC key pairs, to cut down in processing time and have small key sizes.
The system was put in place with JDK 11 some years ago. We discovered when testing with recent JDK versions, 17 and later, the tool was not generating the same CA key pairs as with JDK 11 given the same seed, which then prevents TLS establishment because the CA is not the same anymore.

We tracked down the change to the introduction of EdDSA in JDK15 and as far as we can see, JDK15 to 21 all generate the same key pairs. Interestingly, when applying the same technique to RSA keys, all JDK versions generate the same key pair.

The attached example program exemplifies the tool. An example of output with different JDKs:

 tmp> jdk/jdk-11/bin/java -cp . ECGenKeyPair | grep CA.P
CA Private: CRC32=0CCB2FA2 (3041020100301306072a8648ce3d020106082a8648ce3d0301070427302502010104205a45e7a7571d7f3361307efacefce258f77644c164b370f95fff2aa6df227973)
CA Public: CRC32=4F4D2776 (3059301306072a8648ce3d020106082a8648ce3d030107034200042c0beb5990f3e8b2facf97810df34d393920ab2111f64c8f7754248468572ee2d12ed4796edaea0074859f8c1b00884f1425667e5a069f60b52d6beb43fc4f06)
tmp> jdk/jdk-14.0.1/bin/java -cp . ECGenKeyPair | grep CA.P
CA Private: CRC32=0CCB2FA2 (3041020100301306072a8648ce3d020106082a8648ce3d0301070427302502010104205a45e7a7571d7f3361307efacefce258f77644c164b370f95fff2aa6df227973)
CA Public: CRC32=4F4D2776 (3059301306072a8648ce3d020106082a8648ce3d030107034200042c0beb5990f3e8b2facf97810df34d393920ab2111f64c8f7754248468572ee2d12ed4796edaea0074859f8c1b00884f1425667e5a069f60b52d6beb43fc4f06)

tmp> jdk/jdk-15.0.1//bin/java -cp . ECGenKeyPair | grep CA.P
CA Private: CRC32=899C36A9 (3041020100301306072a8648ce3d020106082a8648ce3d03010704273025020101042029206fa6f59f40d2879facbf78f126fe69264f3738ef209347389a042ba9b7c2)
CA Public: CRC32=83270E8B (3059301306072a8648ce3d020106082a8648ce3d0301070342000469fb77597405ac3cd61dc31ddc3bbd00b977cfa5ca628b027c91fb38ec0e8c0fae064383c19fd6e381ac1dd7b53e1fe97ea9915b94582320682415f5f183267e)
tmp> jdk/jdk-21-rc//bin/java -cp . ECGenKeyPair | grep CA.P
CA Private: CRC32=899C36A9 (3041020100301306072a8648ce3d020106082a8648ce3d03010704273025020101042029206fa6f59f40d2879facbf78f126fe69264f3738ef209347389a042ba9b7c2)
CA Public: CRC32=83270E8B (3059301306072a8648ce3d020106082a8648ce3d0301070342000469fb77597405ac3cd61dc31ddc3bbd00b977cfa5ca628b027c91fb38ec0e8c0fae064383c19fd6e381ac1dd7b53e1fe97ea9915b94582320682415f5f183267e)

Repeatability is important to us because it simplifies key management in product deployment and introduces customers easily into PKI concepts and allows for deployment in mixed version environments.

Is there a way to get back to how JDK11 behaved or make sure this kind of change does not happen again ?

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The attached code examples outputs the results of generating key pairs.
Runs with different JDK versions can be compared to see when there is a difference

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Always have the same key pair
ACTUAL -
Key pair depends on ranges of JDKs the program is run with

---------- BEGIN SOURCE ----------
import static javax.crypto.Cipher.ENCRYPT_MODE;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.zip.CRC32;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

final class KeyEntropyProvider extends SecureRandom {
    private final Cipher cipher;

    KeyEntropyProvider(final byte[] seed) {
        try {
            cipher = Cipher.getInstance("AES/CTR/NoPadding");
            cipher.init(ENCRYPT_MODE, new SecretKeySpec(seed, "AES"), new IvParameterSpec(new byte[16]));
        } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException |
                 NoSuchPaddingException e) {
            // These parameters are supported on OpenJDK, Oracle and IBM JREs.
            throw new RuntimeException(e);
        }
    }

    @Override
    public void nextBytes(final byte[] bytes) {
        try {
            cipher.update(new byte[bytes.length], 0, bytes.length, bytes);
            ECGenKeyPair.hexDump("Random " + bytes.length + " bytes", bytes);
        } catch (ShortBufferException e) {
            // Output size should be equal to input size, hence output buffer size should always be sufficient.
            throw new RuntimeException(e);
        }
    }
}

class ECGenKeyPair {
    public static void main(String[] args) throws Exception {
        System.out.println("JVM Version" + Runtime.version());
        SecureRandom random = new KeyEntropyProvider(new byte [16]);
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
        keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"), random);
        var keypair = keyPairGenerator.generateKeyPair();
        hexDump("CA Private", keypair.getPrivate().getEncoded());
        hexDump("CA Public", keypair.getPublic().getEncoded());

        final KeyPairGenerator masterKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
        masterKeyPairGenerator.initialize(3072, random);
        keypair = masterKeyPairGenerator.generateKeyPair();
        hexDump("Master Key Private", keypair.getPrivate().getEncoded());
        hexDump("Master Key Public", keypair.getPublic().getEncoded());
    }

    public static String byteArrayToHex(byte[] a) {
        StringBuilder sb = new StringBuilder(a.length * 2);
        for(byte b: a)
            sb.append(String.format("%02x", b));
        return sb.toString();
    }

    static void hexDump(String header, byte[] data) {
        CRC32 crc = new CRC32();
        crc.update(data);
        System.out.format("%s: CRC32=%08X (%s)\n", header, crc.getValue(), byteArrayToHex(data));
    }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Do not mix JDK versions, but that is not acceptable :)

FREQUENCY : always



Comments
Additional Information from submitter =========================== Thanks for further testing. Indeed it looks like changes for https://bugs.openjdk.org/browse/JDK-8166597 modified the way IntegerPolynomial encodes byte arrays into long arrays, with cascading consequences, apparently.
24-09-2023

Modified the provided reproducer to observe the changes of crc.getValue() of CA Private and CA Public between different JDKs on Windows 11: JDK 11: Passed. JDK 15ea+23:Passed, values do not change. JDK 15ea+24: Failed, values change JDK 17: Failed. JDK 21: Failed. JDK 22ea+16: Failed.
22-09-2023