Implement Diffie-Hellman key exchange in Java

5.3k views Asked by At

I am trying to implement Diffie-Hellman key exchange in Java, but I'm having a hard time understanding the specification:

Complete the Diffie-Hellman key exchange process as a local mechanism according to JWA (RFC 7518) in Direct Key Agreement mode using curve P-256, dT and QC to produce a pair of CEKs (one for each direction) which are identified by Transaction ID. The parameter values supported in this version of the specification are:

  • “alg”: ECDH-ES
  • “apv”: SDK Reference Number
  • “epk”: QC, in JSON Web Key (JWK) format
  • {“kty”:”EC” “crv”:“P-256”}
  • All other parameters: not present
  • CEK: “kty”:oct - 256 bits

Create a JSON object of the following data as the JWS payload to be signed:

{“MyPublicKey”: “QT”, “SDKPublicKey”:” QC”}

Generate a digital signature of the full JSON object according to JWS (RFC 7515) using JWS Compact Serialization. The parameter values supported in this version of the specification are:

  • “alg”: PS256 or ES256
  • “x5c”: X.5C v3: Cert(MyPb) plus optionally chaining certificates

From my understanding, ECDH will produce a secret key. After sharing my ephemeral public key (QT), the SDK produces the same secret key, so we can later exchange JWE messages encrypted with the same secret key.

The JSON {“MyPublicKey”: “QT”, “SDKPublicKey”:” QC”} will be signed and sent, but I do not understand how I will use apv and epk since these header params are used in JWE and not in the first JWS to be shared.

On the same specification, they talk about these JWE messages, but they do not have these apv and epk parameters.

Encrypt the JSON object according to JWE (RFC 7516) using the same “enc” algorithm used by the SDK, the CEK obtained identified by “kid” and JWE Compact Serialization. The parameter values supported in this version of the specification are:

  • “alg”: dir
  • “enc”: either A128CBC-HS256 or A128GCM
  • “kid”: Transaction ID
  • All other parameters: not present

I also read the example in RFC 7518 where I can see the header params apv and epk being used but I'm not sure which header params, JWE's or JWS's ?

Any thought on how this could be implemented using nimbus-jose-jwt or any other java library would be really helpful. Thanks

3

There are 3 answers

10
Argb32 On

Nimbus (as well as Jose4j) is not the best choice for implementing the specification (I guess it's 3D secure 2.x.x).

These libraries do not return content encrypion key but use it to encrypt or decrypt messages as per JWE spec.

I found that Apache CXF Jose library does its job well as a JWE/JWS/JWK implementation. Except generating ephemeral key pairs. But it's can easily be done with Bouncy Castle:

Security.addProvider(new BouncyCastleProvider());
ECGenParameterSpec ecGenSpec = new ECGenParameterSpec(JsonWebKey.EC_CURVE_P256);
KeyPairGenerator g = KeyPairGenerator.getInstance(ALGORITHM_SIGNATURE_EC, "BC");
g.initialize(ecGenSpec, new SecureRandom());
KeyPair keyPair = g.generateKeyPair();

Content encrypion key can be produced with this code:

byte[] cek = JweUtils.getECDHKey(privateKey, publicKey, null, SDKReferenceNumber, "", 256);

And used to encrypt messages getting JWE compact representation:

JweEncryption jweEncryption = JweUtils.getDirectKeyJweEncryption(cek, contentAlgorithm);
JweHeaders head = new JweHeaders();
head.setHeader(JoseConstants.HEADER_KEY_ID, keyId);
String encrypted = jweEncryption.encrypt(contentToEncrypt.getBytes(StandardCharsets.UTF_8), head);

Or decrypt:

JweCompactConsumer compactConsumer = new JweCompactConsumer(encrypted);
JweHeaders head = compactConsumer.getJweHeaders();
JweDecryption jweDecryption = JweUtils.getDirectKeyJweDecryption(cek, head.getContentEncryptionAlgorithm());
JweDecryptionOutput out = jweDecryption.decrypt(encrypted);
String decrypted = out.getContentText();
0
banan3'14 On

Both apv (Agreement PartyVInfo) and epk (Ephemeral Public Key) are optional, so they can be used in multiple ways. You can use apv to reflect SDK version for example. They are added to the JWE header.

You can read more about JWE in Nimbus here

An example using Nimbus JOSE would be:

import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.KeyOperation;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;

public class Security {

    public void generateJWE() throws JOSEException, URISyntaxException {
        JWEHeader jweHeader = buildJWEHeader(new Base64URL("202333517"), buildECKey());
        JWEObject jweObject = new JWEObject(jweHeader, new Payload("Hello World!"));
    }

    private JWEHeader buildJWEHeader(Base64URL apv, ECKey epk) {
        JWEHeader.Builder jweBuilder = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A128GCM);
        jweBuilder.agreementPartyVInfo(apv);
        jweBuilder.ephemeralPublicKey(epk);
        return jweBuilder.build();
    }

    private ECKey buildECKey() throws URISyntaxException {
        Set<KeyOperation> keyOperations = new HashSet<>();
        keyOperations.add(KeyOperation.ENCRYPT);
        String transactionID = "73024831";
        URI x5u = new URI("https//website.certificate");
        KeyStore keystore = null; //initialize it
        List<Base64> x5c = new ArrayList<>();
        return new ECKey(Curve.P_256, new Base64URL("x"), new Base64URL("y"), KeyUse.ENCRYPTION, keyOperations, Algorithm.NONE, transactionID, x5u, new Base64URL("x5t"), new Base64URL("x5t256"), x5c, keystore);
    }
}

Instead of EncryptionMethod.A128GCM you can use EncryptionMethod.A128CBC-HS256 as in your specification. apv and epk are added to JWEHeader inside builder. Other parameters can be chosen in constructors of JWEHeader.Builder and ECKey. I used ECDH-ES algorithm, A128GCM encryption method, P-256 curve (elliptic curve is default in ECKey generation), transaction ID is a string. I chose other parameters without any clear pattern. Initialization of KeyStore would be too broad for the example. Encryption is only one thing you can do with JWE, among signature and others.

1
Ashish Kirpan On

I able to generate the Secret Key for A128CBC-HS256. However still not succeed in A128GCM. Adding the working sample for A128CBC-HS256 with help nimbus-jose library. This below code is only for key generation.

Please also note that I override the nimbus-jose classes for my usage. Hope it helps.

private void generateSHA256SecretKey(AREQ areq, ECKey sdkPubKey, KeyPair acsKeyPair) throws Exception {

        // Step 4 - Perform KeyAgreement and derive SecretKey
        SecretKey Z = CustomECDH.deriveSharedSecret(sdkPubKey.toECPublicKey(), (ECPrivateKey)acsKeyPair.getPrivate(), null);

        CustomConcatKDF concatKDF = new CustomConcatKDF("SHA-256");

        String algIdString = "";
        String partyVInfoString = areq.getSdkReferenceNumber();
        int keylength = 256; //A128CBC-HS256            

        byte[] algID = CustomConcatKDF.encodeDataWithLength(algIdString.getBytes(StandardCharsets.UTF_8));
        byte[] partyUInfo = CustomConcatKDF.encodeDataWithLength(new byte[0]);          
        byte[] partyVInfo = CustomConcatKDF.encodeDataWithLength(partyVInfoString.getBytes(StandardCharsets.UTF_8));
        byte[] suppPubInfo = CustomConcatKDF.encodeIntData(keylength);
        byte[] suppPrivInfo = CustomConcatKDF.encodeNoData();

        SecretKey derivedKey = concatKDF.deriveKey(
            Z,
            keylength,
            algID,
            partyUInfo,
            partyVInfo,
            suppPubInfo,
            suppPrivInfo);

        System.out.println("Generated SHA256 DerivedKey : "+SecureUtils.bytesToHex(derivedKey.getEncoded()));       
    }