Ephemeral elliptic curve Diffie-Hellman key agreement in Java

Diffie-Hellman key agreement (DH) is a way for two parties to agree on a symmetric secret key without explicitly communicating that secret key. As such, it provides a way for the parties to negotiate a shared AES cipher key or HMAC shared secret over a potentially insecure channel. It does not by itself provide authentication, however, so it is vulnerable to man-in-the-middle attacks without additional measures. There are several ways to provide these additional measures (e.g. signing the ephemeral public keys using a CA-issued certificate, or using a protocol like OTR), but we will not discuss them here, or go into the details of how the key agreement works. Java provides support out-of-the-box for both original discrete log DH and elliptic curve (ECDH) key agreement protocols, although the latter may not be supported on all JREs. ECDH should be preferred for any new applications as it provides significantly improved security for reasonable key sizes.

elliptic curve
An elliptic curve defined by y2 = x3 – 2x + 2

As is often the case in Java, the use of these classes can be a bit convoluted. Here we demonstrate simple Java code for ECDH key agreement on the command line. We only demonstrate ephemeral key agreement, in which the two parties generate unique public/private key pairs at the start of the protocol and throw them away once the shared secret has been negotiated. This can form the basis for perfect forward secrecy.

WARNING: the code here is not a complete security protocol and should be used for reference on the Java API only.

The Code

The complete code example is as follows:

import java.security.*;
import java.security.spec.ECParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.interfaces.ECPublicKey;

import javax.crypto.KeyAgreement;

import java.util.*;
import java.nio.ByteBuffer;
import java.io.Console;

import static javax.xml.bind.DatatypeConverter.printHexBinary;
import static javax.xml.bind.DatatypeConverter.parseHexBinary;

public class ecdh {

  public static void main(String[] args) throws Exception {
    Console console = System.console();
    // Generate ephemeral ECDH keypair
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
    kpg.initialize(256);
    KeyPair kp = kpg.generateKeyPair();
    byte[] ourPk = kp.getPublic().getEncoded();

    // Display our public key
    console.printf("Public Key: %s%n", printHexBinary(ourPk));

    // Read other's public key:
    byte[] otherPk = parseHexBinary(console.readLine("Other PK: "));

    KeyFactory kf = KeyFactory.getInstance("EC");
    X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(otherPk);
    PublicKey otherPublicKey = kf.generatePublic(pkSpec);

    // Perform key agreement
    KeyAgreement ka = KeyAgreement.getInstance("ECDH");
    ka.init(kp.getPrivate());
    ka.doPhase(otherPublicKey, true);

    // Read shared secret
    byte[] sharedSecret = ka.generateSecret();
    console.printf("Shared secret: %s%n", printHexBinary(sharedSecret));

    // Derive a key from the shared secret and both public keys
    MessageDigest hash = MessageDigest.getInstance("SHA-256");
    hash.update(sharedSecret);
    // Simple deterministic ordering
    List<ByteBuffer> keys = Arrays.asList(ByteBuffer.wrap(ourPk), ByteBuffer.wrap(otherPk));
    Collections.sort(keys);
    hash.update(keys.get(0));
    hash.update(keys.get(1));

    byte[] derivedKey = hash.digest();
    console.printf("Final key: %s%n", printHexBinary(derivedKey));
  }
}

To use the example, compile it and then run two instances of it (possibly on different computers). It will print out a hex-encoded ephemeral public key value and then wait for the public key from the other instance. Simply copy+paste this values from one to the other (and vice-versa) and then it will print out the computed shared secret, again hex-encoded and finally a secret key derived from the shared secret.

Let’s walk through the example step-by-step. Firstly, we import a vast number of different classes. We’ll discuss what all of these are for when we get to them.

Step 1: Generate ephemeral ECDH key pair

The first step is to generate an ephemeral elliptic curve key pair for use in the algorithm. We do this using the aptly-named KeyPairGenerator using the “EC” algorithm name to select Elliptic Curve key generation:

    // Generate ephemeral ECDH keypair
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
    kpg.initialize(256);
    KeyPair kp = kpg.generateKeyPair();
    byte[] ourPk = kp.getPublic().getEncoded();

By setting the key size to 256-bits, Java will select the NIST P-256 curve parameters (secp256r1). For other key sizes, it will choose other NIST standard curves, e.g. P-384, P-521. If you wish to use different parameters, then you must specify them explicitly using the ECGenParameterSpec argument. 

Step 2: Exchange the public keys

The next step is to send our public key to the other party and to receive their public key. In this case, we achieve this by simply printing them out and requiring a human being to perform the communication. No authentication is performed. This means that we cannot be sure that the public key we received is really from the other party and not from a man-in-the-middle.

    // Display our public key
    console.printf("Public Key: %s%n", printHexBinary(ourPk));

    // Read other's public key:
    byte[] otherPk = parseHexBinary(console.readLine("Other PK: "));

    KeyFactory kf = KeyFactory.getInstance("EC");
    X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(otherPk);
    PublicKey otherPublicKey = kf.generatePublic(pkSpec);

We assume that the other party is also using a NIST P-256 curve public key. We also assume that the output of PublicKey.getEncoded() is an X.509-encoded key. This turns out to be true in the Oracle JRE, but I cannot find any documented guarantee of this behaviour. A more robust approach would be to communicate the ECPoint and ECParameterSpec of the public key, and use an ECPublicKeySpec to reconstruct the key, but that is even more work.

Step 3: Perform key agreement

The actual ECDH key agreement is straightforward once we have exchanged public keys.

    // Perform key agreement
    KeyAgreement ka = KeyAgreement.getInstance("ECDH");
    ka.init(kp.getPrivate());
    ka.doPhase(otherPublicKey, true);

We grab an instance of the ECDH key agreement protocol. The first step is to initialise it with our private key. Then we pass it the other party’s public key via the doPhase() method. We pass true as the second argument to indicate that this is the last phase of the agreement (it is the only phase in ECDH). Diffie-Hellman works by calculating a shared secret based on our private key and the other party’s public key, so this is all we need in this case. The magic of DH is that each party will calculate the same value despite having different sets of keys available to them. Nobody listening in on the exchange can calculate the shared secret unless they have access to one of the private keys (which are never communicated).

Step 4: Extract the shared secret and derive keys

The final step is to extract the shared secret and then derive a key from it.

Note that it is not advisable to use the shared secret directly as a symmetric key for various reasons. In particular, while the derived secret is indistinguishable from a randomly selected element from the set of all possible outputs of the elliptic curve group, this is not the same thing as a uniformly random string of bits. Viewed as a string of bits, it will have some structure to it. Put another way, the P-256 curve provides roughly equivalent security to a 128-bit secret key, yet the output shared secret is 256 bits. This reveals that the shared secret does not really provide 256 bits of “random” key data. There are further reasons for not using the shared secret directly, depending on the usage. For instance, the security considerations section of RFC 7748 advises to derive a key from the shared secret plus both public keys if we intend to use the key for authentication (this RFC uses different curves than we use here, but it is good advice regardless):

Designers using these curves should be aware that for each public
key, there are several publicly computable public keys that are
equivalent to it, i.e., they produce the same shared secrets. Thus
using a public key as an identifier and knowledge of a shared secret
as proof of ownership (without including the public keys in the key
derivation) might lead to subtle vulnerabilities.

We adopt the approach described in the libsodium documentation, of deriving a key by hashing the shared secret and both public keys, but using SHA-256 rather than BLAKE2. These choices of algorithms and curves are purely for convenience because they are readily available on the JVM without 3rd party libraries (e.g. Bouncy Castle). The only trickiness is to ensure that we feed the public keys into the hash in the same order on both sides of the agreement protocol. For simplicity, we do this by just sorting them lexicographically.

    // Read shared secret
    byte[] sharedSecret = ka.generateSecret();
    console.printf("Shared secret: %s%n", printHexBinary(sharedSecret));

    // Derive a key from the shared secret and both public keys
    MessageDigest hash = MessageDigest.getInstance("SHA-256");
    hash.update(sharedSecret);
    // Simple deterministic ordering
    List<ByteBuffer> keys = Arrays.asList(ByteBuffer.wrap(ourPk), ByteBuffer.wrap(otherPk));
    Collections.sort(keys);
    hash.update(keys.get(0));
    hash.update(keys.get(1));

    byte[] derivedKey = hash.digest();
    console.printf("Final key: %s%n", printHexBinary(derivedKey));

Et voila! Both parties now have the same derived key, which can be used for an AES cipher or HMAC key or however you wish. A more sophisticated key derivation function, such as HKDF, can be used to derive further keys. The JSON Web Token (JWT) specs describe a fully elaborated ECDH key agreement and key derivation approach.

Advertisements

Author: Neil Madden

I am an independent IAM and application security consultant, with particular expertise in ForgeRock's OpenAM access management product. I have over 18 years of professional software development experience in commercial, government and academic settings. I have a PhD and 1st-class honours degree in Computer Science.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s